from twisted.internet import defer, reactor
import random, copy, time
import operator
rng = random.SystemRandom()

from formatting import *

class EngineError(Exception):
	pass
class AbilityError(EngineError):
	pass
class NotReadyError(AbilityError):
	pass
class DisallowedPlaceError(AbilityError):
	pass
class NoMpError(AbilityError):
	pass
class NoTargetError(AbilityError):
	pass
class DeadTargetError(AbilityError):
	pass
class NotLeaderError(EngineError):
	pass
class JoinSelfError(EngineError):
	pass
class GroupFullError(EngineError):
	pass

class Status(object):
	def __init__(self):
		self.name = 'status'
		self.duration = 'perm'
		self.attrmult = {}
		self.attradd = {}
		self.statusflags = {}
		
class Ability(object):
	def __init__(self):
		self.sortorder = 0
		self.usable = set()
		self.targetmode = set()
		self.mpcost = 0
		self.attackattr = None
		self.attackpower = 0
		self.attacktype = None
		self.attackflags = set()
		self.hitverb = 'hits'
		self.hitstring = '%(doer)s %(verb)s %(targ)s for %(dam)s!'
		self.usestring = ''
		
		self.defendattr = None
		
		self.targetstatus = None
		self.usestatus = None
		
	def __cmp__(self,other):
		return cmp(self.sortorder,other.sortorder)
		
	def acquireTargets(self, doer, arg):
		#prototype is single targeting
		if arg.lower() == 'self':
			return [doer]
		try:
			return [doer.location.namemap[arg.lower()]]
		except KeyError:
			raise NoTargetError
		
		
	def finalDamage(self, basedmg, targ, targstat):
		defense = targ.resist(self.attacktype) / targ.getstat(self.defendattr)
		if 'const' in self.attackflags:
			dmg = self.attackpower * defense
		elif 'multcur' in self.attackflags:
			dmg = targ.getstat(targstat) / (1/self.attackpower * defense)
		elif 'multmax' in self.attackflags:
			dmg = targ.getstat('max'+targstat) / (1/self.attackpower * defense)
		else:
			dmg = basedmg * defense
		return int(dmg)

hitability = Ability()
hitability.usable.add('fight')
hitability.attackattr, hitability.defendattr, hitability.attackpower, hitability.attacktype = 'str', 'pdef', 1, 'hit'
hitability.sortorder = 0

guardability = Ability()
guardability.usable.add('fight')
guardability.usestatus = Status()
guardability.usestatus.name = 'guard'
guardability.usestatus.duration = 'turn'
guardability.usestatus.attrmult['pdef'] = 2
guardability.sortorder = 2

class Creature(object):
	name = 'Creature'
	
	hpmax, hp = 200, 200
	mpmax, mp = 0, 0
	str, mag, pdef, mdef, speed = 10, 10, 10, 10, 10
	
	hitverb = 'hits'
	
	location = None
	readyTimer = None
	ready = True
	
	def __init__(self):
		self.inventory = []
		self.status = {}
		self.abilities = {}
		self.group = set()
		
	@property
	def delay(self):
		return max(0, 15 - (self.speed / 2))
	
	def getstat(self, statname):
		stat = getattr(self, statname, None)
		for status in self.status.values():
			if statname in status.attradd:
				stat += status.attradd[statname]
			if statname in status.attrmult:
				stat *= status.attrmult[statname]
		return stat
	def resist(self, damtype):
		resist = 1.0
		for status in self.status.values():
			try:
				resist *= status.statusflags['resist'][damtype]
			except KeyError:
				pass
		return resist
	
	@property
	def colorname(self):
		if self.hp <= 0:
			return colstr( RED, self.name )
		elif self.ready:
			return colstr( GREEN, self.name )
		return self.name
		
	def stopReadyTimer(self):
		if self.readyTimer is not None:
			if self.readyTimer.active():
				self.readyTimer.cancel()
			self.readyTimer = None
	def turnReady(self):	#called by readyTimer when turn is ready
		self.ready = True	#implemented by subclasses too
							#Player sends its owner a line indicating readiness to receive a command
							#Monster calls its ai method
		self.stopReadyTimer()
	def die(self):
		self.location.takeMessage(MSG_GAME_ATTACK, "%s dies!" % self.name)
		self.ready = False
		self.stopReadyTimer()
		
	def takeMessage(self, type, message, speaker=None):
		pass
	def doCommand(self, line, subtable=None):
		command, _, arg = line.partition(' ')
		#self.location.takeMessage(MSG_SYSTEM, 'debug: %s doing command \'%s\' with arg \'%s\'' % (self.name, command, arg))
		
		atable = self.abilities
		if subtable is not None:
			atable = subtable
		
		if subtable is None and command in self.location.commands:
			try:
				self.location.doCommand(self, command, arg)
			except NotLeaderError:
				self.takeMessage(MSG_GAME_ERR, 'Only the group leader can do that.')
		elif subtable is not None and command == '':
			self.takeMessage(MSG_GAME_INFO, self.abilstring(atable) )
		elif command in atable:
			#self.takeMessage(MSG_SYSTEM, '%s in %s' % (command, atable))
			try:
				if atable[command].__class__ is Ability:
					self.location.doAbility(self, atable[command], arg)
				else:
					self.doCommand(arg, atable[command])
			except NotReadyError:
				self.takeMessage(MSG_GAME_ERR, 'You\'re not yet ready to act!')
			except DisallowedPlaceError:
				self.takeMessage(MSG_GAME_ERR, 'Can\'t use \'%s\' here.' % command)
			except NoMpError:
				self.takeMessage(MSG_GAME_ERR, 'Not enough mp.')
			except NoTargetError:
				self.takeMessage(MSG_GAME_ERR, 'No such target \'%s\' here.' % arg)
			except DeadTargetError:
				self.takeMessage(MSG_GAME_ERR, 'Can\'t target dead guy.')
		else:
			self.takeMessage(MSG_GAME_ERR, 'Invalid action \'%s\'.' % command)
	def prompt(self, full=0):
		if full>0:
			self.location.prompt(self)
			
		self.takeMessage( MSG_GAME_INFO, self.statstring())
		
		if full>0 and self.ready:
			abilstring = self.abilstring()
			if len(abilstring) > 0:
				self.takeMessage( MSG_GAME_INFO, abilstring)
				
	def statstring(self):
		parts = {'hp': colstr(RED, '%d/%dhp ' % (self.hp, self.hpmax)), 'mp': '', 'time': ''}
		
		if self.mpmax > 0:
			parts['mp'] = colstr( CYAN, '%d/%dmp ' % (self.mp, self.mpmax), YELLOW)
			
		if self.hp <= 0:
			parts['time'] = colstr( RED, '(dead)' )
		elif self.ready:
			parts['time'] = colstr( '%d;%d' % (GREEN, BOLD), 'READY')
		elif self.readyTimer is not None:
			parts['time'] = colstr( GREEN, '-%.2f' % (self.readyTimer.getTime() - time.time()) )
			
		return '[ %(hp)s%(mp)s%(time)s ]' % parts
	def abilstring(self, subtable = None):
		atable = self.abilities
		if subtable is not None:
			atable = subtable
		abils = [abil for abil in sorted(atable,key=operator.itemgetter(1)) if self.location.allowsAbility(atable[abil])]
		if len(abils) > 0:
			return '{ %s }' % ' | '.join(abils)
		return ''
		
	def copyOther(self, other):
		self.hpmax, self.hp = other.hpmax, other.hp
		self.mpmax, self.mp = other.mpmax, other.mp
		self.str, self.mag, self.pdef, self.mdef, self.speed = other.str, other.mag, other.pdef, other.mdef, other.speed
		self.location = other.location
		self.hitverb = other.hitverb
		
		self.inventory = copy.deepcopy(other.inventory)
		self.status = copy.deepcopy(other.status)
		self.abilities = other.abilities
	
	hitverb = 'hits'


class Player(Creature):
	owner = None	#communications.ChatProtocol
	leader = None	#another Player
	cmdmode = '.'
	def __init__(self, owner):
		super(Player,self).__init__()
		self.owner = owner
		self.name = owner.nickname
		self.gameState = {}
	def takeMessage(self, type, message, speaker=None):
		#do ignore lists etc
		#do other game data substitutions
		self.owner.message(type, message, speaker)
	def doCommand(self, line, subtable=None):
		if subtable is not None:
			super(Player,self).doCommand(line, subtable)
		elif line == '':
			self.prompt(1)
		elif (self.cmdmode==''):
			if line.startswith('.'):
				super(Player,self).doCommand('say ' + line[1:])
			elif line.startswith(','):
				super(Player,self).doCommand('emote ' + line[1:])
			else:
				super(Player,self).doCommand(line)
		else:
			if line.startswith(self.cmdmode):
				super(Player,self).doCommand(line[len(self.cmdmode):])
			elif line.startswith(','):
				super(Player,self).doCommand('emote ' + line[1:])
			else:
				super(Player,self).doCommand('say ' + line)
	
	@property
	def party(self):
		if self.leader is None:
			return self.group
		else:
			return self.leader.group
	
	@property
	def groupLeader(self):
		if self.leader is None:
			return self
		else:
			return self.leader
			
	def joinGroup(self, target):
		if target is self:
			raise JoinSelfError
		if target.leader is not None:
			target = target.leader
		if len(target.group) >= 6:
			raise GroupFullError
		if self.leader is not None:
			self.leaveGroup()
		self.takeMessage(MSG_GAME_EVENT, 'You join %s\'s group.' % target.name )
		for mate in target.group:
			mate.takeMessage( MSG_GAME_EVENT, '%s joined %s group.' % (self.name, (mate is target) and 'your' or 'the') )
		self.leader = target
		self.leader.group.add(self)
		self.group = None
		
	def leaveGroup(self):
		if self.leader is not None:
			self.leader.group.remove(self)
			self.takeMessage(MSG_GAME_EVENT, 'You leave %s\'s group.' % self.leader.name )
			for mate in self.leader.group:
				mate.takeMessage( MSG_GAME_EVENT, '%s left %s group.' % (self.name, (mate is self.leader) and 'your' or 'the') )
		if self.group is not None and len(self.group) > 1:
			self.takeMessage(MSG_GAME_EVENT, 'You disband the group.')
			for mate in self.group.copy():
				if mate is not self:
					mate.takeMessage(MSG_GAME_EVENT, '%s disbanded the group.' % self.name )
					mate.leaveGroup()
		self.leader = None
		self.group = set()
		self.group.add(self)
		
	def turnReady(self):
		super(Player,self).turnReady()
		self.location.takeMessage(MSG_GAME_EVENT, '%s is ready to act.' % self.name, self)
		self.prompt(1)
				
	def quit(self):
		self.leaveGroup()
		self.location.loseCreature(self)
		#save character file
		
class Monster(Creature):
	def __init__(self):
		super(Monster, self).__init__()
		
	def turnReady(self):
		super(Monster,self).turnReady()
		#generalized ai here later
		#self.location.takeMessage( MSG_GAME_EVENT, 'debug: %s is ready.' % self.name )
		while self.ready:
			abil = rng.choice( [ key for key in self.abilities.keys() if self.location.allowsAbility(self.abilities[key]) ] )
			targ = rng.choice( [ creature.name for creature in self.location.creatures if creature.__class__ is Player and creature.hp>0 ] )
			#self.location.takeMessage( MSG_SYSTEM, 'debug: monster abil \'%s\' targ \'%s\' command \'%s\'' % (abil, targ, '%s %s' % (abil, targ)) )
			self.doCommand('%s %s' % (abil, targ))
		
class Role(Creature):
	pass
		
class Place(object):
	def __init__(self):
		self.creatures = set()
		self.namemap = {}
		self.commands = {'say': self.command_say, 'emote': self.command_emote, 'me': self.command_emote, 'look': self.command_look, 's': self.command_status, 'status': self.command_status, 'help': self.command_help, '?': self.command_help}
		
	def takeCreature(self, creature):
		self.creatures.add(creature)
		self.namemap[creature.name.lower()] = creature
		if creature.location:
			creature.location.loseCreature(creature)
		creature.location = self
	def loseCreature(self, creature):
		self.creatures.remove(creature)
		del self.namemap[creature.name.lower()]
	
	def takeMessage(self, type, message, speaker=None):
		for creature in self.creatures:
			creature.takeMessage(type, message, speaker)
			
	def doCommand(self, doer, command, arg):
		#if doer allowed command &c here
		if command in self.commands:
			self.commands[command](doer, arg)
	def command_say(self, speaker, message):
		self.takeMessage(MSG_CHAT, message, speaker)
	def command_emote(self, speaker, message):
		self.takeMessage(MSG_EMOTE, message, speaker)
	def command_look(self, looker, arg):	#implemented by subclasses
		looker.takeMessage(MSG_GAME_INFO, 'You see nothing.')
	def command_status(self, checker, arg):	
		group = checker.party
		checker.takeMessage(MSG_GAME_INFO, '[ Status of %s\'s group: ]' % checker.groupLeader.name)
		maxnamelen = max([len(p.name) for p in group])
		for player in group:
			checker.takeMessage(MSG_GAME_INFO, '[ %*s: ]%s' % ( maxnamelen, player.name, player.statstring() ))
			
	def command_help(self, player, arg):
		player.takeMessage( MSG_GAME_INFO , 'Available game commands: %s' % ' '.join(self.commands) )
		player.takeMessage( MSG_GAME_INFO , 'Your available abilities: %s' % player.abilstring() )
		player.takeMessage( MSG_GAME_INFO , 'For server commands see \'/help\'')
		
			
	def prompt(self, creature):		#implemented by subclasses
		pass
		
	def allowsAbility(self, ability):
		return False
	def doAbility(self, doer, ability, arg):
		if not self.allowsAbility(ability):
			raise DisallowedPlaceError
		if doer.mp < ability.mpcost:
			raise NoMpError
		
		usevars = {'doer': doer.name, 'arg': arg, 'mpcost': colstr(YELLOW, '(%d)' % ability.mpcost) }
		
		if ability.attackpower:
			aflags = ability.attackflags
			if ability.attacktype == 'hit':
				for status in doer.status.values():
					if 'hitflags' in status.statusflags:
						aflags = aflags | status.statusflags['hitflags']
						
			targets = ability.acquireTargets(doer, arg)
			
			if len(targets) > 1:
				doer.mp -= ability.mpcost
				if len(ability.usestring) > 0:
					self.takeMessage(MSG_GAME_ATTACK, ability.usestring % usevars)
			
			for target in targets:
				power = ability.attackpower * doer.getstat(ability.attackattr)
				basedmg = rng.gauss(power * 100, power * 18)
				if target.hp <= 0 and 'lifedeath' not in ability.attackflags:
					raise DeadTargetError
				if len(targets) == 1:
					doer.mp -= ability.mpcost
					if len(ability.usestring) > 0:
						usevars['targ'] = target.name
						self.takeMessage(MSG_GAME_ATTACK, ability.usestring % usevars)
				#reflect
				#evasion
				#cover
				damage = ability.finalDamage(basedmg, target, 'hp')
				
				hitvars = {'doer': doer.name, 'targ': target.name, 'verb': ability.hitverb, 'dam': colstr( damage < 0 and GREEN or RED , abs(damage) ) }
				if ability.attacktype == 'hit':
					hitvars['verb'] = doer.hitverb
				self.takeMessage(MSG_GAME_ATTACK, ability.hitstring % hitvars)

				target.hp -= damage
				#lifedeath flag handling
				target.hp = min(target.hp, target.hpmax)
				if target.hp <= 0:
					target.die()
				else:
					if ability.targetstatus is not None:
						tstatus = copy.deepcopy(ability.targetstatus)
						target.status[tstatus.name] = tstatus
				#stun, steal, clearbuffs, cleardebuffs
				#counter
				if target is not doer:
					target.prompt()
		else:
			doer.mp -= ability.mpcost
			if len(ability.usestring) > 0:
				self.takeMessage(MSG_GAME_ATTACK, ability.usestring % usevars)
		
			
		for turnstatus in [s for s in doer.status if doer.status[s].duration == 'turn']:
			del doer.status[turnstatus]
				
		if ability.usestatus is not None:
			ustatus = copy.deepcopy(ability.usestatus)
			doer.status[ustatus.name] = ustatus
		return True
				
					

class Zone(Place):
	def __init__(self, world, file, name):
		super(Zone, self).__init__()
		self.world = world
		self.file = file
		self.name = name
		self.fightprob = 0
		self.discoveries = {}
		self.lookmsg = []
		self.exit = {}
		self.fightprofiles = {}
		self.flags = set()
		
		self.commands.update({'explore': self.command_explore, 'travel': self.command_travel, 'join': self.command_join, 'leave': self.command_leave, 'heal': self.command_heal})
		
	def prompt(self, creature):
		group = creature.party
		if len(group) > 1:
			creature.takeMessage( MSG_GAME_INFO , '[%s\'s group: %s]' % (creature.groupLeader.name, ', '.join([c.name for c in group]) ))
			
		
	def allowsAbility(self, ability):
		try:
			if 'zone' in ability.usable:
				return True
		except AttributeError:
			if len( [a for a in ability if self.allowsAbility(ability[a])] ) > 0:
				return True
		return False
			
	def command_look(self, looker, arg):
		msg = 'You see nothing.'
		for m in self.lookmsg:
			if True: #evaluate m.req
				msg = m.msg
		looker.takeMessage(MSG_GAME_INFO, msg)
		others = [c.name for c in self.creatures if c is not looker]
		if len(others) == 0:
			others = ['No one.']
		looker.takeMessage(MSG_GAME_INFO, 'Here: %s' % ', '.join(others) )
		#show discoveries, exits
		
	def command_join(self, joiner, arg):
		try:
			joiner.joinGroup(self.namemap[arg.lower()])
		except KeyError:
			joiner.takeMessage(MSG_GAME_ERR, 'They\'re not here.')
		except JoinSelfError:
			joiner.takeMessage(MSG_GAME_ERR, 'You can\'t follow yourself, you\'d just go in circles!')
		except GroupFullError:
			joiner.takeMessage(MSG_GAME_ERR, 'Their group is full.')
	def command_leave(self, leaver, arg):
		if leaver.leader is None and len(leaver.group) <= 1:
			joiner.takeMessage(MSG_GAME_ERR, 'You\'re not in a group.')
		else:
			leaver.leaveGroup()
		
	def command_explore(self, player, arg):
		if player.leader is not None:
			raise NotLeaderError
		possibleDiscoveries = []
		for d in self.discoveries:
			if rng.random() <= d.p: #and player's explore count is high enough
				#evaluate reqs, then if they pass:
				possibleDiscoveries.append(d)
		#if there are any possibleDiscoveries, then we want the player to discover rng.choice(possibleDiscoveries)
		if rng.random() <= self.fightprob:
			fight = Fight(self)
			
			mgroup = set()
			
			nplayers = len(player.group)
			for i in range( 0,rng.randint(nplayers, int(nplayers * 2.5)) ):
				monster = self.world.newMonster('core', 'beast')
				monster.name = 'Beast%d' % (i+1)
				mgroup.add(monster)
				monster.group = mgroup
			
			fight.takeGroup( '%s\'s group' % player.name, player.group )
			fight.takeGroup( 'Monsters', mgroup )
			fight.start()
			
	def command_travel(self, player, arg):
		pass
			
	def command_heal(self, player, arg):	#temporary testing command
		player.hp, player.mp = player.hpmax, player.mpmax
		player.takeMessage(MSG_GAME_EVENT, 'The Goddess mends your wounds and cleanses your spirit.')
		player.prompt()
			
		
class LookMessage(object):
	def __init__(self, msg):
		self.msg = msg
		self.req = None
		
class Discovery(object):
	def __init__(self):
		self.explore = 0
		self.p = 1
		self.lookmsg = []
		self.req = None
		self.consq = None
	
class Fight(Place):
	def __init__(self, zone):
		super(Fight, self).__init__()
		self.zone = zone
		self.groups = {}
		self.dead = set()
		
	def prompt(self, creature):
		for gname in self.groups:
			creature.takeMessage( MSG_GAME_INFO , '[%s: %s]' % (gname, ', '.join([c.colorname for c in self.groups[gname]]) ))
		
	def allowsAbility(self, ability):
		try:
			if 'fight' in ability.usable:
				return True
		except AttributeError:
			if len( [a for a in ability if self.allowsAbility(ability[a])] ) > 0:
				return True
		return False
		
	def doAbility(self, doer, ability, arg):
		if not self.allowsAbility(ability):
			raise DisallowedPlaceError
		if doer.ready:
			if super(Fight,self).doAbility(doer, ability, arg):
				doer.ready = False
				doer.stopReadyTimer()
				doer.readyTimer = reactor.callLater(doer.getstat('delay'), doer.turnReady)
				doer.prompt()
				self.checkForEnd()
		else:
			raise NotReadyError
		
	def takeGroup(self, name, group):
		self.groups[name] = group
		for creature in group:
			self.takeCreature(creature)
			
	def start(self):
		self.takeMessage( MSG_GAME_EVENT , 'A fight starts!')
		for creature in self.creatures:
			creature.ready = False
			creature.readyTimer = reactor.callLater(creature.getstat('delay') / (rng.random() + 1), creature.turnReady)
		for creature in self.creatures:
			creature.prompt(1)
			
	def checkForEnd(self):
		winners = [ g for g in self.groups if len([c for c in self.groups[g] if c.hp>0]) ]
		if len(winners) < 2:
			for w in winners:
				self.takeMessage( MSG_GAME_EVENT , 'Victorious: %s!' % w)
				#dole out exp
			self.end()
	def end(self):
		for creature in self.creatures:
			creature.stopReadyTimer()
		for player in [c for c in self.creatures if c.__class__ is Player]:
			self.zone.takeCreature(player)
			player.hp = max(player.hp, 1)
			player.ready = True
			player.doCommand('%slook' % player.cmdmode)
			player.prompt()

class World(object):
	def __init__(self):
		self.worldFiles = {}
		self.defaultZone = None
		self.roles = {}
	def newPlayer(self, owner):
		char = Player(owner)
		char.group.add(char)
		return char
	def newMonster(self, file, name):
		monster = Monster()
		monster.copyOther(self.worldFiles[file].creatures[name])
		return monster
		
	def assignRole(self, char, rolename):
		try:
			char.copyOther(self.roles[rolename])
			return True
		except KeyError:
			return False
	def roleList(self):
		return '{ %s }' % ' | '.join(self.roles)
	
class WorldFile(object):
	def __init__(self, name):
		self.name = name
		self.zones = {}
		self.creatures = {}
		self.abilities = {}

class PlayerState(object):
	def __init__(self):
		self.owner = None
		self.zone = None
		self.discovered = {}
		self.explore = 0